using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Collections;
using System.Collections.ObjectModel;
using System.Configuration;
using System.IO;
using System.Net;
using System.Security.Policy;
using System.Security.Principal;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using Microsoft.SharePoint;
using Microsoft.Win32;
using log4net;
using net.windward.api.csharp;
using net.windward.utils.ado;
using ArrowCore;
using ArrowScheduleCore;
using ArrowWebParts;
using WRScheduleService.AuthenticationService;
using WRScheduleService.CopyService;
using Microsoft.SharePoint.Administration;

namespace WRScheduleService
{
    public partial class WindwardScheduler : ServiceBase
    {
        #region Fields
        private string emailServer;
        private string emailAddress;
        /// <summary>
        /// The list of scheduled items that need to be processed now.
        /// </summary>
        private ArrayList forceStart = new ArrayList();
        /// <summary>
        /// The worker thread that services the scheduled items.
        /// </summary>
        private Thread thrdDaemon = null;
        /// <summary>
        /// Flag to tell thrdDaemon when to start/stop service.
        /// </summary>
        private bool keepOnTrucking = true;
        /// <summary>
        /// Synchronization signal to tell thrdDaemon when to block/work.
        /// </summary>
        private ManualResetEvent syncControl = null;
        /// <summary>
        /// This is the event log name.
        /// </summary>
        private const string LOG = "WRSchedSvcLog";
        /// <summary>
        /// The event log's source name
        /// </summary>
        private const string LOG_SRC = "WRScheduleService";
        private const string ARROW_EMAIL_SERVER_PROP = "WindwardArrowEmailServer";
        private const string ARROW_EMAIL_ADDR_PROP = "WindwardArrowEmailAddress";
        /// <summary>
        /// the log
        /// </summary>
        private static ILog log = LogManager.GetLogger(typeof(WindwardScheduler));
        #endregion Fields

        #region Constructor
        /// <summary>
        /// Creates a new instance of the scheduler service.
        /// </summary>
        public WindwardScheduler()
        {
            InitializeComponent();
            if (!System.Diagnostics.EventLog.SourceExists(LOG))
                System.Diagnostics.EventLog.CreateEventSource(LOG_SRC, LOG);

            eventLog.Source = LOG_SRC;
            eventLog.Log = LOG;

            log4net.Config.XmlConfigurator.Configure();
        }
        #endregion Constructor

        #region Windows Service Events
        /// <summary>
        /// Starts the schedule service daemon.
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            if (log.IsDebugEnabled)
                log.Debug("scheduler starting");

            // start service daemon
            try
            {
                keepOnTrucking = true;
                syncControl = new ManualResetEvent(false);

                thrdDaemon = new Thread(Daemon);
                thrdDaemon.Name = "ReportDaemon";
                thrdDaemon.Priority = ThreadPriority.BelowNormal;
                thrdDaemon.IsBackground = true;
                thrdDaemon.Start();
            }
            catch (Exception ex)
            {
                ArrowUtils.LogFatalException(log, ex);
                Stop();
            }
        }
        /// <summary>
        /// Loads email settings.
        /// </summary>
        private void LoadEmailSettings()
        {
            emailServer = (string)SPFarm.Local.Properties[ARROW_EMAIL_SERVER_PROP];
            emailAddress = (string)SPFarm.Local.Properties[ARROW_EMAIL_ADDR_PROP];

            if (log.IsDebugEnabled)
                log.Debug(string.Format("email-server: {0}; email-address: {1}", emailServer, emailAddress));
        }
        /// <summary>
        /// Attempts to load the license into the java license property for the engine.
        /// Logs exception upon failure.
        /// </summary>
        private static void tryLoadLicense()
        {
            try
            {
                string strLicense = ArrowLicenseHandler.ReadLicense();
                if (log.IsDebugEnabled)
                    log.Debug(string.Format("loading license: {0}", strLicense));
                ArrowLicenseHandler.LoadJavaLicenseProperty(strLicense);
            }
            catch (Exception ex)
            {
                ArrowUtils.LogFatalException(log, ex);
            }
        }
        /// <summary>
        /// Releases schedule service's resources.
        /// </summary>
        protected override void OnStop()
        {
            if (log.IsDebugEnabled)
                log.Debug("stopping scheduler");

            // stop service
            try
            {
                // kill the daemon
                keepOnTrucking = false;
                if (thrdDaemon != null && thrdDaemon.ThreadState != System.Threading.ThreadState.Unstarted)
                {
                    thrdDaemon.Join(5 * 1000);
                    if (thrdDaemon.IsAlive)
                    {
                        thrdDaemon.Abort();
                        Thread.Sleep(100);
                    }

                    thrdDaemon = null;
                }

                // clean up the sync control
                if (syncControl != null)
                {
                    syncControl.Close();
                    syncControl = null;
                }
            }
            catch (Exception ex)
            {
                ArrowUtils.LogFatalException(log, ex);
            }
        }
        #endregion Windows Service Events

        /// <summary>
        /// This is the main function of the schedule service.
        /// </summary>
        private void Daemon()
        {
            if (log.IsDebugEnabled)
                log.Debug("daemon running for first time");

            try
            {
                //ScheduleManager.IsArrow = true;
                ScheduleManager schedMgr = new ScheduleManager();
                ScheduleRunManager runMgr = new ScheduleRunManager();

                // process scheduled items
                while (keepOnTrucking)
                {
                    // load sleep time minutes from config
                    int sleepTimeMinutes = 1;
                    try
                    {
                        ConfigurationManager.RefreshSection("appSettings");
                        string strSleepTimeMinutes = ConfigurationManager.AppSettings["SleepTimeMinutes"];
                        string logVal = string.IsNullOrEmpty(strSleepTimeMinutes) ? "NULL" : strSleepTimeMinutes;
                        if (log.IsDebugEnabled)
                            log.DebugFormat("SleepTimeMinutes value in web.config: {0}", logVal);
                        sleepTimeMinutes = int.Parse(strSleepTimeMinutes.Trim());
                        if (sleepTimeMinutes < 1)
                        {
                            sleepTimeMinutes = 1;
                            if (log.IsWarnEnabled)
                                log.Warn("SleepTimeMinutes values less than 1 minute are not allowed.  Using 1 minute as default.");
                        }
                    }
                    catch (Exception ex)
                    {
                        Exception logEx = new SchedulerConfigurationException("Invalid SleepTimeMinutes entry in app.config. Only integer values of 1 or more are allowed.", ex);
                        ArrowUtils.LogWarnException(log, logEx);
                    }

                    int sleepTimeMilliseconds = sleepTimeMinutes * 60 * 1000;
                    try
                    {
                        tryLoadLicense();

                        LoadEmailSettings();

                        ConfigurationManager.RefreshSection("connectionStrings");
                        string connErrMsg = ScheduleManager.CanCreateConnection(true);
                        if (connErrMsg == null) // if the database exists...
                        {
                            // first any requested events
                            while (forceStart.Count > 0)
                            {
                                long id;
                                lock (forceStart)
                                {
                                    id = (long)forceStart[0];
                                    forceStart.RemoveAt(0);
                                }
                                ScheduleItem item = schedMgr.FindById(id);
                                if (item != null)
                                {
                                    if (log.IsDebugEnabled)
                                        log.Debug(string.Format("force starting item: {0}", item.Id));
                                    RunItem(schedMgr, runMgr, item);
                                }
                            }

                            // run all reports past due
                            ScheduleItemCollection coll = schedMgr.FindAllScheduleDue();
                            foreach (ScheduleItem item in coll)
                            {
                                try
                                {
                                    if (log.IsDebugEnabled)
                                        log.Debug(string.Format("Running past due item: {0}", item.Id));
                                    RunItem(schedMgr, runMgr, item);
                                }
                                catch (Exception ex)
                                {
                                    // log the exception, but continue on with the next item
                                    ArrowUtils.LogFatalException(log, ex);
                                }

                                // if killed or paused, stop processing
                                if (!keepOnTrucking)
                                    return;
                            }

                            //bugbug: we still run this to keep compatibility with this code, but we
                            // override the time below
                            // find out how long to pause. Always pause at least 1 minute
                            ScheduleItem next = schedMgr.FindNext();
                            if (next == null)
                            {
                                sleepTimeMilliseconds = sleepTimeMinutes * 60 * 1000;
                                //if (log.IsDebugEnabled)
                                //    log.Debug(string.Format("no items, pausing {0} milliseconds", sleepTimeMilliseconds));
                            }
                            else
                            {
                                TimeSpan ts = next.RunNext - DateTime.UtcNow;
                                if (ts.TotalMilliseconds > 24 * 60 * 60 * 1000) // 24 hrs cap
                                    sleepTimeMilliseconds = 24 * 60 * 60 * 1000;
                                else
                                    sleepTimeMilliseconds = Math.Max((int)ts.TotalMilliseconds, 60 * 1000); //bugbug: verify this...
                                //if (log.IsDebugEnabled)
                                //    log.Debug(string.Format("item found, pausing {0} milliseconds", sleepTimeMilliseconds));
                            }
                        }
                        else
                        {
                            throw new SchedulerConfigurationException(connErrMsg);
                        }
                    }
                    catch (Exception ex)
                    {
                        ArrowUtils.LogWarnException(log, ex);
                    }
                    finally
                    {
                        //bugbug: (2/26/2010); scheduler pauses one minute after every run; must remain this way
                        // until a messaging system is setup so that arrow wakes scheduler when new schedule items
                        // are added
                        //bugbug: (9/14/2010); adding app.config SleepTimeMinutes to adjust sleep time
                        //else   //enjayh
                        //{   //enjayh
                        //bugbug: debug only; when no longer needed in debug, uncomment the three lines with 'enjayh' so it still checks every minute if a database is not found
                        sleepTimeMilliseconds = sleepTimeMinutes * 60 * 1000;
                        //if (log.IsDebugEnabled)
                        //    log.Debug(string.Format("no db found; pausing {0} msec before looking again", milli));
                        //}   //enjayh
                        if (log.IsDebugEnabled)
                            log.DebugFormat("pausing {0} milliseconds", sleepTimeMilliseconds);
                        syncControl.WaitOne(sleepTimeMilliseconds, false);
                        syncControl.Reset();
                    }
                }
            }
            catch (Exception ex)
            {
                ArrowUtils.LogFatalException(log, ex);
                Stop();
            }
        }

        #region Methods
        /// <summary>
        /// Runs a scheduled report.
        /// </summary>
        /// <param name="schedMgr"></param>
        /// <param name="runMgr"></param>
        /// <param name="item"></param>
        private void RunItem(ScheduleManager schedMgr, ScheduleRunManager runMgr, ScheduleItem item)
        {
            if (log.IsDebugEnabled)
                log.DebugFormat("running scheduled item: {0}", item.Id);

            // get the site URL
            String siteUrl = item.SiteUrl;
            if (log.IsDebugEnabled)
                log.DebugFormat("system = {0}; siteUrl = {1}", item.System, siteUrl);

            // run the report and save the run log
            ScheduleRunItem sri =  runFromSPReportDefinition(siteUrl, item);

            // bugbug: should this be done in runFromSPReportDefinition now?
            // save the report to sharepoint, unless exception
            if (sri.ReportStream != null)
            {
                try
                {
                    SaveReport(item, sri.ReportStream);
                }
                catch (Exception ex)
                {
                    ArrowUtils.LogWarnException(log, ex);
                    // bugbug: add message to sri; sri doesn't allow that at this time (1/3/10)
                }
            }

            // save the log item
            runMgr.Insert(sri);

            // save the schedule run info
            item.CalculateRunNext();
            schedMgr.Update(item);
            if (log.IsDebugEnabled)
                log.Debug("RunItem: finished running scheduled item");
        }
        /// <summary>
        /// Saves a report using settings from the specified ScheduleItem.
        /// </summary>
        /// <param name="item"></param>
        /// <param name="sri"></param>
        /// <param name="saveSiteUrl"></param>
        private static void SaveReport(ScheduleItem item, Stream reportStream)
        {
            //SaveReportOM(item, reportStream);
            SaveReportSvc(item, reportStream);
        }
        /// <summary>
        /// Saves the report using SharePoint OM.
        /// </summary>
        /// <param name="item"></param>
        /// <param name="reportStream"></param>
        /// <param name="saveSiteUrl"></param>
        private static void SaveReportOM(ScheduleItem item, Stream reportStream)
        {
            string saveSiteUrl = item.SaveSiteUrl;
            using (SPSite siteCollection = new SPSite(saveSiteUrl))
            {
                SPWeb site = siteCollection.OpenWeb();
                SPFolder saveFolder = site.GetFolder(item.SaveFolder);
                string libRelPath = saveFolder.ServerRelativeUrl;
                string libPath = siteCollection.MakeFullUrl(libRelPath);
                string documentPath = libPath + "/" + item.Title + "." + ExtensionConvert.ToExtension(item.Format);
                if (log.IsDebugEnabled)
                    log.DebugFormat("saving to: {0}", documentPath);
                site.Files.Add(documentPath, reportStream, true);
                site.Dispose();
            }
        }
        /// <summary>
        /// Saves the report using web services.
        /// </summary>
        /// <param name="item"></param>
        /// <param name="reportStream"></param>
        /// <param name="saveSiteUrl"></param>
        private static void SaveReportSvc(ScheduleItem item, Stream reportStream)
        {
            string saveSiteUrl = item.SaveSiteUrl;
            using (Authentication authSvc = new Authentication())
            {
                // prep the auth service
                authSvc.Url = string.Format("{0}/_vti_bin/authentication.asmx", saveSiteUrl);
                authSvc.CookieContainer = new CookieContainer();
                authSvc.AllowAutoRedirect = true;
                authSvc.Credentials = CredentialCache.DefaultNetworkCredentials;

                // get the auth mode
                AuthenticationMode authMode = authSvc.Mode();

                // throw for unsupported auth modes
                if (authMode == AuthenticationMode.Passport)
                    throw new Exception("Passport authentication not currently supported.");
                if (authMode != AuthenticationMode.Windows && authMode != AuthenticationMode.Forms)
                    throw new Exception("Scheduler could not authenticate.");

                // save report using copy service
                using (Copy copySvc = new Copy())
                {
                    // prep copy service
                    copySvc.Url = string.Format("{0}/_vti_bin/copy.asmx", saveSiteUrl);

                    // set copy service credentials/cookie based on auth mode
                    if (authMode == AuthenticationMode.Windows)
                        copySvc.Credentials = CredentialCache.DefaultNetworkCredentials;
                    else if (authMode == AuthenticationMode.Forms)
                    {
                        // get the username/password from config file
                        string fbaUser = ConfigurationManager.AppSettings["FBAUsername"];
                        string fbaPass = ConfigurationManager.AppSettings["FBAPassword"];

                        // login and grab an auth cookie
                        LoginResult result = authSvc.Login(fbaUser, fbaPass);
                        if (result.ErrorCode != LoginErrorCode.NoError)
                            throw new Exception("Scheduler could not authenticate.");

                        // put the auth cookie into copy service's cookie container
                        copySvc.CookieContainer = authSvc.CookieContainer;
                    }
                    else
                        throw new Exception("Scheduler could not authenticate.");

                    // copy service parameters
                    FieldInformation fieldInfo = new FieldInformation();
                    FieldInformation[] fieldInfoArray = { fieldInfo };
                    CopyResult[] copyResultArray;
                    string fileExt = ExtensionConvert.ToExtension(item.Format);
                    string outFileUrl = string.Format("{0}/{1}.{2}", item.SaveFolder, item.Title, fileExt);
                    string[] destinationUrls = { outFileUrl };
                    string sourceUrl = item.TemplateFileName;

                    uint uploadResult = copySvc.CopyIntoItems(sourceUrl, destinationUrls, fieldInfoArray, ((MemoryStream)reportStream).GetBuffer(), out copyResultArray);
                    if (uploadResult != 0 || copyResultArray.Length < 1 || copyResultArray[0].ErrorCode != CopyErrorCode.Success)
                    {
                        string copySvcMsg = "No error message from copy service.";
                        if (copyResultArray.Length > 0)
                            copySvcMsg = copyResultArray[0].ErrorMessage;
                        throw new Exception(string.Format("Scheduler could not save generated report, {0}. {1}", outFileUrl, copySvcMsg));
                    }
                }
            }
        }
        /// <summary>
        /// processes the specified report Definition
        /// </summary>
        /// <param name="siteUrl">sharepoint site of the report Definition specification file</param>
        /// <param name="item">ScheduleItem for this report Definition</param>
        /// <returns></returns>
        private ScheduleRunItem runFromSPReportDefinition(String siteUrl, ScheduleItem item)
        {
            // get the site
            SPSite siteCollection = new SPSite(siteUrl);
            SPWeb site = siteCollection.OpenWeb();

            // get the rpt Definition
            SPFile reportDefinitionSPFile = null;
            using (SPSite rptDefSiteCol = new SPSite(item.TemplateFileName))
                using (SPWeb rptDefWeb = rptDefSiteCol.OpenWeb())
                    reportDefinitionSPFile = rptDefWeb.GetFile(item.TemplateFileName);
            string filename = reportDefinitionSPFile.Name;
            string extension = filename.Substring(filename.LastIndexOf('.') + 1);
            ReportDefinition reportDefinition;
            MemoryStream reportDefinitionStream = new MemoryStream(reportDefinitionSPFile.OpenBinary(), false);
            if ("wrf".Equals(extension) || "reportdef".Equals(extension))
            {
                reportDefinition = new ReportDefinition(reportDefinitionStream);
            }
            else
            {
                string errorMessage;
                reportDefinition = ArrowUtils.GetReportDefFromFile(reportDefinitionSPFile, item.TemplateFileName, out errorMessage, site.ID);
            }
            // get the template
            //bugbug: wrong site; use site from template url

            SPFile tplSPFile = null;
            using (SPSite templateSiteCol = new SPSite(reportDefinition.template))
                using (SPWeb templateWeb = templateSiteCol.OpenWeb())
                    tplSPFile = templateWeb.GetFile(reportDefinition.template);
            MemoryStream template = new MemoryStream(tplSPFile.OpenBinary(), false);

            ScheduleRunItem sri = null;

            // run the report
            if ("wrf".Equals(extension) || "reportdef".Equals(extension))
            {
                Dictionary<string, IReportDataSource> dataSources = extractDataBindings(reportDefinition);

                string oldFilename = item.TemplateFileName;
                item.TemplateFileName = reportDefinition.template;
                sri = RunScheduledReport.Run(item, template, dataSources);
                item.TemplateFileName = oldFilename;
            }
            else
            {
                //bugbug: why do we use reportDefinitionStream here instead of template?
                TemplateMetrics tm = Report.GetMetrics(reportDefinitionStream);

                // run the report
                string oldFilename = item.TemplateFileName;
                item.TemplateFileName = reportDefinition.template;
                sri = RunScheduledReport.Run(item, template, tm);
                item.TemplateFileName = oldFilename;
            }

            // cleanup
            site.Dispose();
            siteCollection.Dispose();
            
            // return the run log
            return sri;
        }
        /// <summary>
        /// Extracts the <see cref="DataSourceBindingCollection"/> from a report definition.
        /// </summary>
        /// <param name="rptDef"></param>
        /// <returns>Dictionary of data sources. Keys are the names. Empty string is used for the data source with no name.</returns>
        private Dictionary<string, IReportDataSource> extractDataBindings(ReportDefinition rptDef)
        {
            Dictionary<string, IReportDataSource> dataSources = new Dictionary<string, IReportDataSource>();
            for (int dataInd = 0; dataInd < rptDef.dataSourceNameArray.Length; dataInd++)
            {
                string dataDefPath = rptDef.dataSourcePathArray[dataInd];
                DataDefinition dataDef = new DataDefinition(dataDefPath);
                string dsNickname = rptDef.dataSourceNameArray[dataInd] == null ? "" : rptDef.dataSourceNameArray[dataInd];
                IReportDataSource ds = dataDef.createIReportDataSource();
                dataSources.Add(dsNickname, ds);
            }
            return dataSources;
        }
        #endregion Methods
    }

    class SchedulerConfigurationException : Exception
    {
        public SchedulerConfigurationException()
            : base()
        { }

        public SchedulerConfigurationException(string message)
            : base(message)
        { }

        public SchedulerConfigurationException(string message, Exception innerException)
            : base(message, innerException)
        { }
    }
}
